class _BaseState(object):
    
    @staticmethod
    def enter(self):
        pass
    
    @staticmethod
    def exit(self):
        pass
    
    @staticmethod
    def think(self, dt):
        pass
    
class ReadyState(_BaseState):
    
    @staticmethod
    def think(self, dt):
        if self.ammo is not None:
            self.ammo -= self.use_per_tick
    
    def __str__(self):
        return 'ready'

class StrikeState(_BaseState):
    
    @staticmethod
    def enter(self):
        self._speed = self.speed
    
    @staticmethod
    def think(self, dt):
        self._speed -= dt
        if self._speed <= 0:
            for baddy in self.current_targets[:]:
                baddy.hurt(self.direct_damage)
                if baddy.health <= 0:
                    self.current_targets.remove(baddy)
            if self.dot_duration:
                track_dot(self, self.damage_over_time, self.dot_duration, self.dot_interval, self.current_targets)
            if self.ammo is not None:
                self.ammo -= self.use_per_tick
                self.ammo -= self.use_per_attack
            return self.state_cooldown
    
    def __str__(self):
        return 'strike'

class CooldownState(_BaseState):
    
    @staticmethod
    def enter(self):
        self._cooldown = self.cooldown
#        self._dot_interval = self.dot_interval
#        self._dot_duration = self.dot_duration
    
    @staticmethod
    def think(self, dt):
        if self.ammo is not None:
            self.ammo -= self.use_per_tick
        self._cooldown -= dt
        if self._cooldown <= 0:
            return self.state_ready
    
    def __str__(self):
        return 'cooldown'

class EmptyState(_BaseState):
    """Weapon has no ammo. Must call Weapon.refill to break out of this state.
    """
    
    @staticmethod
    def think(self, dt):
        if self.ammo > 0:
            return self.state_ready
    
    def __str__(self):
        return 'empty'

##------------------------------------------------------------------------------

def run_dots(dt):
    for attack in dots.keys():
        dot = dots[attack]
        dot_damage,dot_duration,dot_interval,elapsed_interval,baddies = dot
        if len(baddies) == 0:
            del dots[attack]
        elif dot_duration > 0:
            # Count down the dot interval and apply a dot tick.
            elapsed_interval -= dt
            if elapsed_interval <= 0.0:
                # Apply damage, reset the interval, decrement the duration.
                for baddy in baddies[:]:
                    baddy.hurt(dot_damage)
                    if baddy.health <= 0:
                        baddies.remove(baddy)
                elapsed_interval += dot_interval
                dot_duration -= 1
            dots[attack] = [dot_damage,dot_duration,dot_interval,elapsed_interval,baddies]
        else:
            del dots[attack]
def track_dot(attack, dot_damage, dot_duration, dot_interval, baddies):
    dot = [dot_damage,dot_duration,dot_interval,dot_interval,baddies[:]]
    dots[attack] = dot
dots = {}

class Attack(object):
    
    state_ready = ReadyState()
    state_strike = StrikeState()
    state_cooldown = CooldownState()
    state_empty = EmptyState()
    
    def __init__(self,
        speed, cooldown,
        use_per_attack, use_per_tick,
        direct_damage,
        damage_over_time, dot_interval, dot_duration,
        hit_multiple=False,
    ):
        """
        Note:
            - speed, cooldown, dot interval work on elapsed time (dt), which is
              a fraction of a second. For example, a quarter of a second in
              real-time is 0.25.
            - dot duration is number of repetitions (int).
        
        Arguments:
            speed (time it takes to stab/swing/fire, after which direct damage
                is applied)
            cooldown (time it takes to recover, until next attack is ready)
            ammo used per attack (i.e. bullets, gasoline rev)
            ammo used per tick (i.e. gasoline idle)
            direct damage (applied once after waiting speed)
            damage over time (applied once per tick when cooldown starts)
            DoT interval (time between applications of damage over time)
            DoT duration (number of repetitions to apply damage over time)
            hit multiple (self-explanatory)
        """
        self.speed = speed
        self.cooldown = cooldown
        self.use_per_attack = use_per_attack
        self.use_per_tick = use_per_tick
        self.direct_damage = direct_damage
        self.damage_over_time = damage_over_time
        self.dot_interval = dot_interval
        self.dot_duration = dot_duration
        self.hit_multiple = hit_multiple
        
        # initial state
        self._current_state = self.state_ready
        self.current_targets = []
        self._current_state.enter(self)
        
        # runtime for states
        self.ammo = 0
        self._speed = 0
        self._cooldown = 0
#        self._dot_interval = 0
#        self._dot_duration = 0

    @property
    def state(self):
        """Return a string describing the current state.
        """
        return str(self._current_state)
    
    @property
    def dot(self):
        """Returns True if a DoT is in progress, else False.
        """
        return self in dots
    
    def launch(self, others):
        """Attempts to launch a new attack. Returns True if successful.
        Otherwise returns False. A new attack cannot be launch until the current
        attack has completed (current state is attack_ready).
        """
        if not others:
            return []
        if self._current_state is self.state_ready:
            if self.hit_multiple:
                self.current_targets = others[:]
            else:
                self.current_targets = others[0:1]
            self.set_state(self.state_strike)
            return self.current_targets[:]
        else:
            return []
    
    def refill(self, ammo):
        self.ammo = ammo
    
    def set_state(self, new_state):
        if __debug__: print self.__class__.__name__ + str(id(self)), "changing state", self._current_state.__class__.__name__, " -> ", new_state.__class__.__name__
        self._current_state.exit(self)
        self._current_state = new_state
        self._current_state.enter(self)
    
    def think(self, dt, ammo):
        self.ammo = ammo
        new_state = self._current_state.think(self, dt)
        if new_state:
            self.set_state(new_state)
        run_dots(dt)
        if self.ammo is None:
            pass
        elif self.ammo <= 0 and self._current_state is not self.state_empty:
            self.set_state(self.state_empty)
        return self.ammo

class Weapon(object):
    
    def __init__(self, ammo, *default_attack, **attacks):
        """Construct a weapon.
        
        default_attack is a list of length 1. It's value will be kept as the
        default attack (key 'default').
        
        attacks is a dict of {'name':Attack} items, which will be added to the
        weapon as named attacks.
        """
        self.ammo = ammo
        self.attacks = {}
        self._current_attack = None
        
        for attack_name,attack_obj in attacks.items():
            self.add_attack(attack_obj, attack_name)
            self._current_attack = attack_obj
        for attack_obj in default_attack:
            self.add_attack(attack_obj)
            self._current_attack = attack_obj
    
    @property
    def current_attack(self):
        """The current attack that is ready or in progress.
        """
        return self._current_attack
    
    @property
    def state(self):
        """Return a string describing the current state.
        """
        attack = self._current_attack
        if attack is None:
            if self.ammo is None:
                return 'ready'
            elif self.ammo <= 0:
                return 'empty'
        else:
            return attack.state
    
    def add_attack(self, attack_obj, attack_name='default'):
        """Add and instance of Attack to the weapon (e.g. when building a weapon
        or player gains a new skill with it).
        """
        self.attacks[attack_name] = attack_obj
        if self._current_attack is None:
            self._current_attack = attack_obj
    
    def attack(self, others, attack_name='default'):
        """Launch an attack. If weapon state is not "ready" this is ignored.
        """
        attacked = []
        if self.current_attack.state in ('ready','empty'):
            attack = self.attacks[attack_name]
            attacked = attack.launch(others)
            if attacked:
                self._current_attack = attack
        return attacked
    
    def refill(self, ammo):
        """Refill ammo with amount. This is ignored if weapon doesn't take ammo.
        """
        if self.ammo is not None:
            self.ammo = ammo
    
    def think(self, dt):
        """ Call once per tick with dt=milliseconds since last call.
        """
        self.ammo = self.current_attack.think(dt, self.ammo)

##------------------------------------------------------------------------------

if __name__ == '__main__':
    class Baddie(object):
        def __init__(self, name):
            self.name = name
            self.health = 10
        def hurt(self, amount):
            print self.name, 'hit:',amount
            self.health -= amount
            if self.health <= 0:
                print '(dead) What a cruel fate!'
    
    # Note:
    #   - speed, cooldown, dot interval work on elapsed time (dt), which is
    #     a fraction of a second. For example, a quarter of a second in
    #     real-time is 0.25.
    #   - dot duration is number of repetitions (int).
    #
    # 0: ammo (number of attacks; None is unlimited)
    # 1: speed (time it takes to stab/swing/fire, after which direct damage occurs)
    # 2: cooldown (time it takes to recover, until next attack is ready)
    # 3: ammo used per attack (i.e. bullets, gasoline rev)
    # 4: ammo used per tick (i.e. gasoline idle)
    # 5: direct damage (applied once after waiting speed)
    # 6: damage over time (applied once per tick when cooldown starts)
    # 7: DoT interval (time between applications of damage over time)
    # 8: DoT duration (number of repetitions to apply damage over time)
    #
    #                               ammo  ammo      ...dot...
    #                ammo, spd cool  atk  tick  dd    int dur  name
    sword         = (None, .2,.4,    0,    0,  8, 0,  0,  0, 'sword')
    poison_dagger = (None, .1,.1,    0,    0,  4, 1, .1,  6, 'poison dagger')
    hammer        = (None, .5,.5,    0,    0, 10, 0,  0,  0, 'hammer')
    flame_thrower = (10,   .1,.1,    5,0.001,  0, 4, .1, 10, 'flame thrower')
    holy_water    = (5,    .1,.1,    1,    0,  0, 8, .1, 10, 'holy water')
    
    joe = Baddie('Joe')
    def make_weapon(data):
        ammo,speed,cool,ammo_atk,ammo_tick,dd,dot,dot_int,dot_dur,name = data
        w = Weapon(
            ammo,
            Attack(speed,cool,ammo_atk,ammo_tick,dd,dot,dot_int,dot_dur)
        )
        return w,name
    for data in sword,poison_dagger,hammer,flame_thrower,holy_water:
        weap,name = make_weapon(data)
        print '===',name,'starting ammo',weap.ammo,'==='
        joe.health = 10
        weap.attack([joe])
        while 1:
            # .025 seconds == 25 ticks per second
            print 'tick', weap.state
            weap.think(.025)
            if weap.state == 'ready':
                if not weap.current_attack.dot:
                    print 'Done'
                    break
            if weap.current_attack.state == 'empty':
                print 'Out of ammo'
                break
        print '===',name,'final ammo', weap.ammo, '==='
